package yyy.ab.modules.security.cfgBean;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import yyy.ab.common.core.ResultBean;
import yyy.ab.common.utils.ResponseUtil;
import yyy.ab.common.utils.http.HttpUtils;
import yyy.ab.modules.security.*;
import yyy.ab.modules.security.code.sms.DefaultSmsSender;
import yyy.ab.modules.security.code.sms.SmsCodeSender;
import yyy.ab.modules.security.handler.AbAccessDeniedHandler;
import yyy.ab.modules.security.handler.AbAuthenticationFailureHandler;
import yyy.ab.modules.security.handler.AbAuthenticationSuccessHandler;
import yyy.ab.modules.security.properties.AbSecurityProperties;
import yyy.ab.modules.security.session.AbInvalidSessionStrategy;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.IOException;

/**
 * 自定义Web权限配置适配器
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {

    /**
     * 数据源
     */
    @Autowired
    private DataSource dataSource;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private AbAuthenticationProvider abAuthenticationProvider;

    @Autowired
    private AbSecurityProperties securityProperties;

    @Autowired
    private AbAccessDeniedHandler accessDeniedHandler;

    @Autowired
    private SessionRegistry sessionRegistry;

    @Autowired
    private SessionAuthenticationStrategy sessionAuthenticationStrategy;

    @Bean
    public AbAuthenticationFailureHandler failureHandler() {
        return new AbAuthenticationFailureHandler(securityProperties.getLoginErrorUrl());
    }

    @Bean
    public AbAuthenticationSuccessHandler successHandler() {
        return new AbAuthenticationSuccessHandler(securityProperties.getLoginSuccessUrl());
    }

    @Bean
    public InvalidSessionStrategy invalidSessionStrategy(){
        AbInvalidSessionStrategy febsInvalidSessionStrategy = new AbInvalidSessionStrategy();
        febsInvalidSessionStrategy.setSecurityProperties(securityProperties);
        return febsInvalidSessionStrategy;
    }

    @Bean
    @ConditionalOnMissingBean(SmsCodeSender.class)
    public SmsCodeSender smsCodeSender() {
        return new DefaultSmsSender();
    }

    /**
     * 没有任何权限都能访问的地址
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        //这里要设置自定义认证
        auth.authenticationProvider(abAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
                .authorizeRequests();

        String[] defaultUrl = new String[]{"/login", "logout", "error", "400", "403", "404", "500", "/image/code", "/sms/code"};
        registry.antMatchers(defaultUrl).permitAll();
        //除配置文件忽略路径其它所有请求都需经过认证和授权
        for (String url : securityProperties.getIgnored().getUrls()) {
            registry.antMatchers(url).permitAll();
        }

        registry
                .and()
                    //表单登录方式
                    .formLogin()
                    //登录页面url
                    .loginPage("/login")
                    //登录请求url
                    .loginProcessingUrl("/checkLogin")
                    //成功登录处理器
                    .successHandler(this.successHandler())
                    //失败登录处理器
                    .failureHandler(this.failureHandler())
                    .permitAll()
                .and()
                    //允许网页iframe
                    .headers().frameOptions().sameOrigin()//.disable()
                .and()
                    //登出
                    .logout()
                    .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                    .logoutSuccessUrl(securityProperties.getLogoutUrl())
                    .invalidateHttpSession(false)//安全退出不删除会话
                    .permitAll()
                .and()
                    .authorizeRequests()
                    //任何请求
                    .anyRequest()
                    //需要身份认证
                    .authenticated()
                .and()
                    //关闭跨站请求防护
                    .csrf().disable()
                    //自定义权限拒绝处理类
                    .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
                    .authenticationEntryPoint(new AbAuthenticationEntryPoint())
                .and()//session过期失效的地址指向dashboard，原因是暂时处理不了/session-invalid的问题
                    .sessionManagement() // 配置 session管理器
                    .invalidSessionUrl("/dashboard")
                    //.invalidSessionStrategy(invalidSessionStrategy()) // 处理 session失效
                    .maximumSessions(securityProperties.getSession().getMaximumSessions()) // 最大并发登录数量
                    .expiredUrl("/dashboard")
                    .sessionRegistry(sessionRegistry)// 配置 session注册中心
                    //.expiredSessionStrategy(new AbExpiredSessionStrategy()) // 处理并发登录被踢出
                .and()
                    .sessionFixation()
                    .migrateSession()
                    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                    .sessionAuthenticationStrategy(sessionAuthenticationStrategy)
                .and()
                    .rememberMe()//记住我功能
                    .userDetailsService(userDetailsService)//设置用户业务层
                    .tokenRepository(persistentTokenRepository())//设置持久化token
                    .tokenValiditySeconds(24 * 60 * 60);//记住登录1天(24小时 * 60分钟 * 60秒)
    }

    /**
     * 记住我功能，持久化的token服务
     *
     * @return
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        //数据源设置
        tokenRepository.setDataSource(dataSource);
        //启动的时候创建表，这里只执行一次，第二次就注释掉，否则每次启动都重新创建表
        //tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }

    /**
     * 自定义认证进入点
     * ajax：返回请求错误状态
     * 页面：调整登录页
     */
    class AbAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request,
                             HttpServletResponse response,
                             AuthenticationException authException) throws IOException {
            response.setCharacterEncoding("utf-8");
            if (HttpUtils.isAjax(request)) {
                ResponseUtil.out(response, ResultBean.errorMsg("请登录"));
            } else {
                response.sendRedirect(securityProperties.getLoginUrl());//"/login"
            }
        }
    }
}
